#!/usr/bin/perl -w

use strict;

my $range = $ARGV[0] || 12+11;

my %g_max_consecutive_intervals = (1 => 1, 2 => 3, 3 => 1);
my $g_max_interval = 2;

my $n_max_attempts = 20;

# "gsp" variables: state variables used in should_print().
my $gsp_c_printed_scales = 0;
my $gsp_c_attempted_scales = 0;
my %gsp_min_score = ('white_keys' => 1, 'octaves' => 1);
my %gsp_seen_scale;
my %gsp_seen_intervals;

while ( $gsp_c_printed_scales < $n_max_attempts and $gsp_c_attempted_scales < 10000 )
{
    my @scale = &random_scale($range);
    my %score;
    ($score{'white_keys'}, $score{'transposition'}) = &score_white_keys(@scale);
    $score{'octaves'} = &score_octaves(@scale);

    next unless &should_print(\%score, \@scale);

    @scale = map { ($_+$score{'transposition'}) } @scale;
    my @major_scale_anchors = &major_scale_anchors(@scale);

    ++$gsp_c_printed_scales;
    printf '%2d %3d %2.3f %2.3f %s [%s]%s',
    $gsp_c_printed_scales,
    $gsp_c_attempted_scales,
    $score{'white_keys'},
    $score{'octaves'},

    join(' ', map { $_%12} @scale),
    join(', ', map { $scale[$_] } @major_scale_anchors),

    "\n";
}

exit;

sub should_print
{
    my ($hr_score, $ar_scale) = @_;
    my %score = %{$hr_score};
    my @scale = @{$ar_scale};

    ++$gsp_c_attempted_scales;

    return 0 if $gsp_seen_scale{join ':', @scale}++;
    my @these_seen_intervals;
    for my $i (0..$#scale)
    {
	my @intervals = map { ($scale[($i+$_+1)%@scale] - $scale[($i+$_)%@scale]) % 12 } (0..$#scale);
	#warn join ' ', 'scale', @scale, ': ', 'intervals', @intervals;
	my $intervals = join ' ', @intervals;
	return 0 if $gsp_seen_intervals{$intervals};
	push @these_seen_intervals, $intervals;
    }
    map { $gsp_seen_intervals{$_}++ } @these_seen_intervals;
    #die;

    return 0 unless $score{'white_keys'} <= $gsp_min_score{'white_keys'};
    $gsp_min_score{'white_keys'} = $score{'white_keys'};

#    if ( $score{'white_keys'} < $gsp_min_score{'white_keys'} )
#    {
#	$gsp_min_score{'octaves'} = 1;
#    }
#
#    $gsp_min_score{'octaves'} = $score{'octaves'};

    return 1; # for now.

#    next unless $score{'white_keys'} < $gsp_min_score{'white_keys'} or $gsp_c_printed_scales < $n_max_attempts/2;
}

sub random_scale
{
    my ($range) = @_;
    $range ||= 12;
    my @s = (0);
    while ( $s[$#s]-$s[0] <= $range )
    {
	# Max consecutive intervals.
	my @forbidden_intervals = ();
	if ( scalar(@s) > 1 )
	{
	    my $prev_interval = $s[$#s] - $s[$#s-1];
	    if ( $s[$#s]-$s[$#s-$g_max_consecutive_intervals{$prev_interval}] == $g_max_consecutive_intervals{$prev_interval}*$prev_interval )
	    {
		push @forbidden_intervals, $s[$#s]-$s[$#s-1];
	    }
	}
	my $step_size;
	while ( 1 )
	{
	    $step_size = 1 + int(rand $g_max_interval);
	    next if grep { $step_size eq $_ } @forbidden_intervals;
	    last;
	}
	push @s, $s[$#s] + $step_size;
    }
    pop @s;
    return @s;
}

sub score_white_keys
# Give a score telling how well a sequence of notes can be fit to the white keys.
{
    my @scale = @_;

    my @scores = ();
    for my $transposition (0..11)
    {
	push @scores, &score_white_keys_literal(map { ($_ + $transposition)%12 } @scale);
    }
    my $i_max = 0;
    map { $i_max = $_ if $scores[$_] > $scores[$i_max] } (1..$#scores);
    return ($scores[$i_max], $i_max) if wantarray;
    return $scores[$i_max];
}

sub score_white_keys_literal
{
    my @scale = @_;
    my @white_keys = qw(0 2 4 5 7 9 11);

    my ($white, $black) = (0, 0);
    for ( @scale )
    {
	my $note = $_ % 12;
	if ( grep { $note eq $_ } @white_keys )
	{
	    ++$white;
	}
	else
	{
	    ++$black;
	}
    }
    return $white/($white+$black);
}

sub score_octaves
{
    my @scale = @_;
    my %seen = ();
    my ($c_octaves, $c_total) = (0, 0);
    for (@scale)
    {
	++$c_total;
	++$c_octaves if $seen{$_ % 12}++;
    }
    return 2*$c_octaves/$c_total;
}

sub major_scale_anchors
{
    my @scale = @_;
    my @major_scale_interval = qw(0 2 4 5 7 9 11 12);
    my @major_scale_interval_backwards = qw(0 1 3 5 7 8 10 12);
    my @length_major_scale_from;
  SCALE_START:
    for my $i (0..$#scale-1)
    {
	$length_major_scale_from[$i] = 0;
      SCALE_END:
	for my $j ($i+1..$#scale)
	{
	    #die join(':', @scale) . "; $i, $j" if $j-$i > $#major_scale_interval;
	    last SCALE_END if $j-$i > $#major_scale_interval;
	    #warn "($scale[$j] - $scale[$i]) == $major_scale_interval[$j-$i]";
	    last SCALE_END unless ($scale[$j] - $scale[$i]) == $major_scale_interval[$j-$i];
	    ++$length_major_scale_from[$i];
	}
      SCALE_END_BACKWARDS:
	for my $j (reverse(0..$i-1))
	{
	    last SCALE_END_BACKWARDS if $i-$j > $#major_scale_interval_backwards;
	    #warn "[$i, $j] ($scale[$i] - $scale[$j]) == $major_scale_interval_backwards[$i-$j]";
	    last SCALE_END_BACKWARDS unless ($scale[$i] - $scale[$j]) == $major_scale_interval_backwards[$i-$j];
	    $length_major_scale_from[$i] += 0.5;
	}
    }
    #print "scale: ", join(' ', @scale), "\n";
    #print "length_major_scale_from: ", join(' ', @length_major_scale_from), "\n";
    #exit;
    return 
      sort { $length_major_scale_from[$b] <=> $length_major_scale_from[$a] or $a <=> $b } 
	grep { $length_major_scale_from[$_] >= 3 }
	  (0..$#length_major_scale_from);
}
